feat: HTTP/2 multiplexing on relay leg with idempotency-safe h1 fallback#799
Conversation
|
First and foremost, thank you so much for this outstanding commit! Implementing H2 multiplexing and smoothly handling the 421 fallbacks to solve the Head-of-Line (HOL) blocking is a massive win for the project's performance. Looking at how beautifully H2 optimizes the transport layer, I strongly believe that combining it with the native payload multiplexing proposed in my Semi-xmux RFC (#794) would turn MHRV into an absolute powerhouse, specifically regarding speed, latency, and quota preservation. Here is why combining your H2 implementation with Semi-xmux is the ultimate next step: Zero Latency Penalty & Fewer TLS Handshakes: Right now, opening a new connection for every single micro-request introduces significant TLS handshake overhead and latency. By using Semi-xmux to pack multiple internal connections into a single native MHRV stream, we completely eliminate the latency of establishing those repetitive connections. Everything simply moves much faster. Accelerated Speeds & Lower EETB: Because we are drastically reducing the number of required TLS handshakes, the Estimated End-to-End Time to Byte (EETB) drops significantly. Pages will load faster, and the overall responsiveness will feel instantaneous. Drastic Quota Optimization: While H2 makes the TCP/TLS connection highly efficient, Semi-xmux optimizes the actual payload. Packing 4 to 8 connections into one means we hit the GAS endpoint far less frequently, saving a massive amount of the daily 20k quota. To put it simply: H2 gives us a multi-lane highway, and Semi-xmux puts the requests into buses instead of single-occupant cars. The result is zero traffic jams, instant speeds, and a fraction of the GAS quota burned. Whenever you have a moment, I’d love for you to take a look at the proposal in #794 to see how we can build this chunking logic on top of your fantastic H2 foundation. Keep up the great work! Best regards |
|
i think this would be the right answer: |
|
I'm running this branch locally and so far no issues, could download some small files (~10mb) that were previously impossible to download. Wish we could use google drive for downloading large files or offloading some of load unto drive (e.g youtube) |
|
@dazzling-no-more — read through the architecture in detail. The h2 design is solid:
180 → 197 lib tests (+17 covering ALPN selection, sticky disable, RequestSent classification on RST_STREAM, 421 handling, gzip parity with h1, POST body transmission, redirect chain through prod entry, force_http1 round-trip). Tunnel-node tests still 35 green. UI release-build clean. The ready-phase-timeout-as- Strategically this is the architectural fix for the perceived-slowness regression in #781 — h2 multiplexing makes the entire TLS pool stuff (PR #751 changes) much less load-bearing because one connection serves all requests with no head-of-line blocking. If h2 negotiates, the freshest-first/refill-loop machinery just falls out of the picture. Merging — will ship in v1.9.15. If anything goes sideways in the wild, Thanks — this is a meaty, well-instrumented change. [reply via Anthropic Claude | reviewed by @therealaleph] |
…Actions full tunnel docs Wraps four already-merged PRs into a release: - PR #799 (@dazzling-no-more): HTTP/2 multiplexing on the relay leg with idempotency-safe h1 fallback. ALPN-negotiates h2; one TCP/TLS connection multiplexes ~100 streams instead of the pool. Slow Apps Script calls no longer head-of-line-block the queue on the same socket. force_http1 kill switch in config. 180→197 tests (+17). - PR #805 (@yyoyoian-pixel): block_quic default true. QUIC over the TCP-based tunnel was TCP-over-TCP meltdown; browsers fall back to TCP/HTTPS within seconds when UDP/443 is dropped. Adds Android + desktop UI toggles. - PR #819 (@brightening-eyes): enabled accesskit on eframe so screen readers (NVDA/JAWS/VoiceOver/Orca) can navigate the desktop UI. Closes #750. - PR #783 (@euvel): GitHub Actions Full tunnel docs + workflow YAML files for users who can't buy a VPS. cloudflared Quick / ngrok / cloudflared Named. Strategically: h2 multiplexing is the architectural fix for #781 / #773 perceived-slowness regression — it makes the pool tuning machinery much less load-bearing. force_http1 kill switch is there if anything goes sideways in the wild. Tests: 197 lib + 35 tunnel-node green. UI release-mode build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Adds HTTP/2 multiplexing on the Apps Script relay leg. ALPN-negotiates
h2against the Google edge; if the peer agrees, all relay traffic (Apps Script direct, exit-node outer call, full-mode tunnel single ops, full-mode tunnel batches) rides one TCP/TLS connection multiplexing ~100 concurrent streams instead of the legacy 8-80-socket pool. Falls back to the existing HTTP/1.1 keep-alive path automatically when h2 isn't viable.The motivating win: a slow Apps Script call no longer head-of-line-blocks the rest of the queue on the same socket. Most user-visible on streaming sites (YouTube/googlevideo) and concurrent fan-out (range-parallel downloads).
What changed
Transport —
src/domain_fronter.rsClientConfig. Separate h1-onlyTlsConnectorfor the fallback pool so pooled sockets always speak the protocol the rawHTTP/1.1\r\n…writer expects.H2Cell { send, created, generation }+ensure_h2()with: bounded open timeout (8 s), failure backoff (15 s),try_lockopen-dedup so concurrent callers during an outage fall through to h1 immediately instead of serializing behind a slow handshake.poison_h2_if_gen(gen)only clears the cell when generation matches — protects against a stale failure clobbering a freshly-reopened healthy cell.Safety
RequestSent::{No, Maybe}carried out of every h2 failure.Nocovers anything beforesend_requestsucceeds (URI build, ready, send_request err, ready timeout).Maybeis anything after.FronterError::NonRetryable(Box<FronterError>)wrapsMaybefailures for non-idempotent methods. Bothdo_relay_with_retryand the exit-node→direct fallback inrelay()checkis_retryable()and skip replay. This closes a gap where an h2 POST that may have reached Apps Script could be re-issued 2-3× by outer retry layers.h2_round_trip: ready bounded at 5 s (constant, classifiedNo), response phase bounded by caller-supplied deadline (H2_RESPONSE_DEADLINE_DEFAULT_SECS = 20for relay,self.batch_timeoutfor tunnel paths so userrequest_timeout_secstuning applies).Config + UI —
src/config.rs,src/bin/ui.rs,android/app/src/main/java/com/therealaleph/mhrv/ConfigStore.ktforce_http1: boolkill switch (default false). Round-trips end-to-end through both desktop UI (FormState+ConfigWire::from) and Android (MhrvConfig+loadFromJson+save).Telemetry
h2_calls/h2_fallbacks/h2_disabledonStatsSnapshot, surfaced infmt_lineash2-success=N/total (X%)and in the AndroidNative.statsJsonschema doc with the explicit caveat that h2 health =h2_calls / (h2_calls + h2_fallbacks)(NOT comparable torelay_calls, which only sees the Apps-Script-direct path).finalize_batch_responsenow redact body content (status + length only, raw body gated behindRUST_LOG=trace) — both h2 and h1 routes share the same finalizer.Android UTC → PT alignment
PT day/روز (PT). The Rust side has used Pacific Time (matching Apps Script's actual quota reset) for a while; Android docs and labels were stale.Configuration
{ // default false; flip to true to disable h2 entirely if a // specific deployment, fronting domain, or middlebox refuses it "force_http1": false }Test plan
cargo test --lib— 197 passed, 0 failed (180 → 197, +17 new tests covering ALPN selection, sticky disable, generation-protected poisoning, RequestSent classification on real RST_STREAM/dead conns, 421 sticky-disable + counter rebalancing, NonRetryable wrapper transparency, gzip decode parity with h1, POST body actually transmitted, redirect chain via production entry point, force_http1 round-trip through Config)cargo build --bin mhrv-rs— cleancargo check --features ui --bin mhrv-rs-ui— cleancargo checkintunnel-nodesub-crate — cleancargo clippy --lib --bin mhrv-rs --no-deps— no warnings on touched filesKill switch
If anything goes sideways in the wild, set
"force_http1": trueinconfig.json(desktop) or hand-edit the Android config and the entire h2 path is bypassed. The h1 keep-alive pool path is unchanged from pre-PR behavior.Documented coverage gap
The ready-phase-timeout-as-
RequestSent::Nopath isn't deterministically testable because h2 0.4 enforces remoteMAX_CONCURRENT_STREAMSatsend_requesttime rather thanreadytime, so a "saturate the slots, expect ready to block" setup races down the response-phase path instead. The ready-arm code is small (single match arm withRequestSent::Noliterally written next to the timeout error) and has an inline comment explaining the gap.